Trabajo Preparacion, visualizacion de Datos y Machine learning con Python

Ciencia de datos en Produccion

Estudiante: Sebastián Cardona y Jose Miguel Millán

ID: 1094910122 y 1088334182

Email: sacardonar@uqvirtual.edu.co y josem.millanl@uqvirtual.edu.co

Docente: [Jose R. Zapata](https://joserzapata.github.io)

  • https://joserzapata.github.io
  • https://twitter.com/joserzapata
  • https://www.linkedin.com/in/jose-ricardo-zapata-gonzalez/

Contenido

  • 1  Definir el Problema a Resolver
    • 1.1  Describir los datos de entrada y salida
  • 2  Importar Librerias
  • 3  Cargar Datasets
  • 4  Descripcion y Limpieza de los datos
    • 4.1  Identificacion de Variables
    • 4.2  Analisis General Univariable y Bivariable
    • 4.3  Eliminar columnas de datos Innecesarios
    • 4.4  Remover Datos Duplicados Exactos
    • 4.5  Procesamiento de Datos Faltantes
      • 4.5.1  Borrar Filas
      • 4.5.2  Reemplazar datos faltantes con la Media/ Moda/ Mediana (Mean/ Mode/ Median Imputation)
    • 4.6  Remover Datos Duplicados Exactos
    • 4.7  Analisis Univariable
      • 4.7.1  Variables Numericas
      • 4.7.2  Variables Categoricas
    • 4.8  Analisis Bivariable
      • 4.8.1  Numericas vs Numericas
      • 4.8.2  Categoricas vs Categoricas
      • 4.8.3  Categoricas vs Numericas
    • 4.9  Procesamiento de Outliers
      • 4.9.1  Deteccion de Outliers (Univariables y Bi variables)
      • 4.9.2  Remover Outliers
    • 4.10  Procesamiento de Outliers
      • 4.10.1  Deteccion de Outliers (Univariables y Bi variables)
      • 4.10.2  Remover Outliers
    • 4.11  Feature Engineering
      • 4.11.1  Transformacion de Variables
        • 4.11.1.1  Normalizacion
        • 4.11.1.2  Escalamiento
        • 4.11.1.3  Logaritmica
        • 4.11.1.4  raíz cuadrada / cúbica
        • 4.11.1.5  Binning , Cambios de Numericas a Categoricas
    • 4.12  Analisis Univariable y Bivariable Final
      • 4.12.1  Creacion de Variables
        • 4.12.1.1  Crear Variables derivadas de Otras
        • 4.12.1.2  Crear Variables de Categorico a Numerico
    • 4.13  Reduccion de Dimensionalidad y Seleccion de Variables (PCA)
    • 4.14  Balance de datos
  • 5  MODELAMIENTO DE LOS DATOS (MACHINE LEARNING)
    • 5.1  Dividir el dataset en Training set y Test set
  • 6  Validacion y Evaluacion Cruzada (k-fold Cross Validation)
  • 7  Optimizacion de Hiper parametros (Hyper Parameter optimization)
  • 8  Evaluacion final del modelo con el Test set
  • 9  Implementacion del Modelo (Deploying)
  • 10  Comunicacion de Resultados (Data Story Telling)
  • 11  Conclusiones
  • 12  Ayudas Y Referencias

Objetivo del Trabajo

En el anterior experimento se entrenaron dos modelos de regresión considerando unos casos atípicos dentro del análisis, los modelos resultantes tuvieron un bajo desempeño, por lo tanto, en este experimento se eliminarán los datos atípicos encontrados.

Definir el Problema a Resolver¶

El dataset "house data", inicialmente se realizará una exploración de datos, para poder saber la calidad del dataset, iniciando con una limpieza la cual consta de eliminar duplicados, identificación de datos atípicos, nullos o mal escritos para poder tratarlos y mitigarlos, ya sea con la eliminación o aplicación de métodos estadísticos, con la finalidad de tener un datset listo y poder aplicar una regresión lineal y poder predecir los precios de venta de una casa.

Describir los datos de entrada y salida¶

  • Cantidad de Variables
  • Tipo de Variables
  • Significado de cada Variable

Importar Librerias¶

In [1]:
from datetime import datetime
from pathlib import Path

import numpy as np
import pandas as pd
import plotly.express as px
import seaborn as sns
from IPython.display import display, HTML
from cache_to_disk import cache_to_disk
from pandas_profiling.profile_report import ProfileReport
from sklearn.linear_model import SGDRegressor, Lasso, Ridge, LinearRegression, ElasticNet
from sklearn.metrics import r2_score
from sklearn.model_selection import cross_val_score, train_test_split, cross_validate, RepeatedKFold
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.svm import SVR
import phik
from tqdm import tqdm

from src.data.preprocessing import preprocessing
from src.features.build_features import build_features
from src.jutils.data import DataUtils
from src.jutils.visual import Plot
In [2]:
# Funciones utilizadas para realizar un preprocesamiento general.

def validar_duplicados(_df):
    _filas = _df.shape[0]
    _cant_duplicados = _df.duplicated().sum()
    print(f'De {_filas} registros hay {_cant_duplicados} filas duplicadas, representando el {_cant_duplicados/_filas:.2%}')

def eliminar_duplicados(_df):
    # Eliminando duplicados
    _df = _df.drop_duplicates(keep='first')
    _filas = _df.shape[0]
    print(f'Después de la eliminación de duplicados, el conjunto de datos queda con {_filas} filas.')
    return _df

def validar_index_duplicados(_df):
    # Validando duplicados de index
    _son_duplicados = _df['index'].duplicated()
    _cant_duplicados = _son_duplicados.sum()
    _filas = _df.shape[0]
    print(f'De {_filas} registros, hay {_cant_duplicados} registros con index duplicado, que representan el {_cant_duplicados/_filas:.2%}.')
    return _son_duplicados

def convertir_col_date_a_date(_df):
    _df['date'] = pd.to_datetime(_df['date'], errors='coerce')
    return _df

def reemplazar_valores_extremos(_df, _columnas_numericas):
    _df[_columnas_numericas] = _df[_columnas_numericas].where(lambda x: x > -1e+10, other=np.nan).where(
        lambda x: x < 1e+10, other=np.nan)
    return _df

def reemplazar_nulos_por_la_media(_df, _columnas_numericas):
    # Se reemplazan los valores nulos por la media Nota: No se considera que haya data leakage pues los valores
    # reemplazados son entre registros con el mismo index y como al final se va a dejar un dataset con index únicos,
    # no hay riesgo que estén tanto en el set de entrenamiento como en el de test
    for columna_numerica in _columnas_numericas:
        _df[columna_numerica] = _df[columna_numerica].fillna(
            _df.groupby('index')[columna_numerica].transform('median'))
    return _df

def reemplazar_fechas_nulas(_df):
    # Reemplazando fechas nulas por la primera fecha no nula
    _df['date'] = _df['date'].fillna(
        _df.groupby(['index'], sort=False)['date'].apply(lambda x: x.ffill().bfill()))
    return _df

def reemplazar_ceros_por_nulos(_df):
    # Reemplazando ceros por valores nulos
    _df[['sqft_basement', 'yr_renovated']] = _df[['sqft_basement', 'yr_renovated']].replace(0, np.nan)
    return _df
In [3]:
# Funciones utilizadas para procesar únicamente los datos de entrenamiento
def z_score_outliers(_df, _column):
    """
    Returns:
        zscore, outlier
    """
    # Adaptado de https://www.kaggle.com/code/shweta2407/regression-on-housing-data-accuracy-87
    #creating lists to store zscore and outliers 
    zscore = []
    isoutlier =[]
    # for zscore generally taken thresholds are 2.5, 3 or 3.5 hence i took 3
    threshold = 3
    # calculating the mean of the passed column
    mean = np.mean(_df[_column])
    # calculating the standard deviation of the passed column
    std = np.std(_df[_column])
    for i in _df[_column]:
        z = (i-mean)/std
        zscore.append(z)
        #if the zscore is greater than threshold = 3 that means it is an outlier
        isoutlier.append(np.abs(z) > threshold)
    return zscore, isoutlier
In [4]:
# Funciones para realizar el procesamiento de los datos antes de ingresar al modelo
def mediana_recortada_imputacion(_df, _column, _isoutlier):
    mediana_recortada = _df[_column][~_isoutlier].median()
    _df.loc[_isoutlier, _column] = mediana_recortada
    return _df

def calculo_variables_adicionales(_df):
    _df['tiene_sotano'] = (~_df['sqft_basement'].isna()).astype(int)
    _df['fue_renovada'] = (~_df['yr_renovated'].isna()).astype(int)
    _df['yr_date'] = _df['date'].dt.year
    _df['antiguedad_venta'] = _df['yr_date'] - _df['yr_built']
    return _df

def procesamiento_datos_faltantes(_df, _columnas):
    _df = _df.dropna(subset=_columnas)
    return _df

def clasificar_columnas(_df, _clasificacion_columnas):
    _df[_clasificacion_columnas['categorica_ordinal']] = _df[_clasificacion_columnas['categorica_ordinal']].astype(int)
    _df[_clasificacion_columnas['numerica_continua']] = _df[_clasificacion_columnas['numerica_continua']].astype(float)
    _df[_clasificacion_columnas['numerica_discreta']] = np.floor(_df[_clasificacion_columnas['numerica_discreta']])
    return _df
    
def imputacion_de_datos(_df, _columnas):
    _df[_columnas] = _df[_columnas].fillna(0)
    return _df
In [5]:
@cache_to_disk(1)
def profiler_to_file(_profiler, archivo):
    print('Ejecutando profiler')
    _profiler.to_file(du.data_folder_path.parent.joinpath('reports/' + archivo))
    return True

def calcular_descriptivas(_df):
    descriptivas = _df.describe()
    descriptivas.loc['rango'] = descriptivas.loc['max'] - descriptivas.loc['min']
    descriptivas.loc['IQR'] = descriptivas.loc['75%'] - descriptivas.loc['25%']
    descriptivas.loc['coef de var'] = descriptivas.loc['std']/descriptivas.loc['mean']
    descriptivas.loc['skewness'] = du.data.skew(numeric_only=True)
    descriptivas.loc['kurtosis'] = du.data.kurtosis(numeric_only=True)
    return descriptivas

plot = Plot()

Cargar Datasets¶

In [6]:
du = DataUtils(
    Path(r'..\data').resolve().absolute(),
    "kc_house_dataDS.parquet",
    'price',
    lambda path: pd.read_parquet(path),
    lambda df, path: df.to_parquet(path)
)
du.data = du.load_data(du.interim_path.joinpath(du.input_file_name))

Descripcion General del Dataset¶

  • numero de filas y columnas
  • tipos de datos y si estan correctos

Durante la exploración inicial se realizó la conversión de los tipos de datos, y la correcta representación de datos nulos.

In [7]:
shape = du.data.shape
filas = shape[0]
columnas = shape[1]
print(f'El conjunto de datos se compone de {filas} filas y {columnas} columnas.')
El conjunto de datos se compone de 131994 filas y 23 columnas.
In [8]:
du.data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 131994 entries, 0 to 131993
Data columns (total 23 columns):
 #   Column         Non-Null Count   Dtype  
---  ------         --------------   -----  
 0   index          131994 non-null  int64  
 1   zipcode        118787 non-null  float64
 2   grade          118717 non-null  float64
 3   sqft_basement  118688 non-null  float64
 4   view           118854 non-null  float64
 5   bathrooms      118689 non-null  float64
 6   bedrooms       118729 non-null  float64
 7   sqft_above     118712 non-null  float64
 8   sqft_living15  118814 non-null  float64
 9   lat            118731 non-null  float64
 10  waterfront     118730 non-null  float64
 11  floors         118791 non-null  float64
 12  date           127544 non-null  object 
 13  yr_renovated   118728 non-null  float64
 14  yr_built       118712 non-null  float64
 15  long           118760 non-null  float64
 16  jhygtf         118728 non-null  float64
 17  sqft_lot       118763 non-null  float64
 18  price          118661 non-null  float64
 19  condition      118740 non-null  float64
 20  wertyj         131994 non-null  int64  
 21  sqft_lot15     118739 non-null  float64
 22  sqft_living    118768 non-null  float64
dtypes: float64(20), int64(2), object(1)
memory usage: 24.2+ MB

Todas las columnas son del tipo correcto a excepción de date, se deberá hacer la conversión de este campo.

Limpieza de calidad de datos general¶

  • Filas con valores exactamente iguales (duplicados)
  • Columnas duplicadas
  • Columnas con valores constantes o sin informacion
In [9]:
validar_duplicados(du.data)
De 131994 registros hay 1300 filas duplicadas, representando el 0.98%
In [10]:
du.data = eliminar_duplicados(du.data)
Después de la eliminación de duplicados, el conjunto de datos queda con 130694 filas.
In [11]:
# Validar indices duplicados
son_duplicados = validar_index_duplicados(du.data)
De 130694 registros, hay 108695 registros con index duplicado, que representan el 83.17%.
In [12]:
# Revisando los primeros registros duplicados
du.data[son_duplicados].sort_values(by='index').head()
Out[12]:
index zipcode grade sqft_basement view bathrooms bedrooms sqft_above sqft_living15 lat ... yr_renovated yr_built long jhygtf sqft_lot price condition wertyj sqft_lot15 sqft_living
44480 0 98178.0 7.0 0.0 0.0 1.0 NaN 1.180000e+03 1.340000e+03 47.5112 ... 0.0 1.955000e+03 NaN 0.0 5.650000e+03 221900.0 3.000000e+00 876543 5650.0 1180.0
59332 0 98178.0 7.0 NaN NaN 1.0 3.0 1.180000e+03 1.340000e+03 47.5112 ... 0.0 1.955000e+03 -122257.0 0.0 5.650000e+03 NaN 3.000000e+00 876543 5650.0 1180.0
89550 0 NaN NaN 0.0 0.0 1.0 3.0 1.180000e+03 NaN NaN ... 0.0 NaN -122257.0 0.0 5.650000e+03 221900.0 3.000000e+00 876543 5650.0 1180.0
27240 0 98178.0 7.0 0.0 0.0 1.0 3.0 1.180000e+03 1.340000e+03 47.5112 ... 0.0 1.955000e+03 -122257.0 0.0 5.650000e+03 221900.0 3.000000e+00 876543 5650.0 1180.0
50303 0 98178.0 7.0 0.0 0.0 1.0 3.0 -5.432346e+10 -5.432346e+10 47.5112 ... 0.0 -5.432346e+10 -122257.0 0.0 -5.432346e+10 221900.0 -5.432346e+10 876543 5650.0 1180.0

5 rows × 23 columns

Revisando los registros duplicados por index, se encuentra que muchas columnas tienen los mismos valores , lo único que cambia es que hay algunos faltantes y hay otros valores extremadamente bajos o altos, adicionalmente se observan algunos registros de la columna date que no son fechas. Primero se convertirá los valores de la columna date a date y los que no puedan ser convertidos se reemplazarán por valores nulos, luego se reemplazarán los valores extremos por valores nulos, luego se calculará la mediana por index para las columnas numéricas y se reemplazarán los valores nulos por estas medianas. Luego se eliminarán filas duplicadas y se reevaluarán los index duplicados.

Si nuestra suposición es correcta, no importa realizar una imputación por la mediana pues todos los valores de los índices son iguales, luego de hacer la imputación y eliminar nuevamente duplicados, no deberían quedar índices duplicados, en caso de que sigan habiendo índices duplicados se deben revertir las imputaciones realizadas y buscar otra estrategia para eliminar duplicados exactos.

In [13]:
# Convirtiendo la columna date a datetime
du.data = convertir_col_date_a_date(du.data)
In [14]:
# Reemplazando valores extremos, menores a -1e+10 o mayores a 1e+10
columnas_numericas = [columna for columna in du.data.columns if columna != 'date']
du.data = reemplazar_valores_extremos(du.data, columnas_numericas)
In [15]:
# Se reemplazan los valores extremos por la media
# Nota: No se considera que haya data leakage pues los valores reemplazados son entre registros con el mismo index y como 
# al final se va a dejar un dataset con index únicos, no hay riesgo que estén tanto en el set de entrenamiento como en el de
# test
du.data = reemplazar_nulos_por_la_media(du.data, columnas_numericas)
In [16]:
# Reemplazando fechas nulas por la primera fecha no nula
du.data = reemplazar_fechas_nulas(du.data)

Para las siguientes columnas, un cero representa un dato nulo, por lo tanto se reemplazarán.

  • sqft_basement
  • yr_renovated
In [17]:
# Reemplazando ceros por valores nulos
du.data = reemplazar_ceros_por_nulos(du.data)
In [18]:
validar_duplicados(du.data)
De 130694 registros hay 108695 filas duplicadas, representando el 83.17%
In [19]:
du.data = eliminar_duplicados(du.data)
Después de la eliminación de duplicados, el conjunto de datos queda con 21999 filas.
In [20]:
son_duplicados = validar_index_duplicados(du.data)
De 21999 registros, hay 0 registros con index duplicado, que representan el 0.00%.

Una vez validados los índices duplicados, se evidencia que la limpieza surtió efecto (Se debe implementar un control que valide esto cuando se vaya a realizar un reentrenamiento, se debe alertar cuando sigan habiendo índices duplicados e interrumpir el proceso)

In [21]:
# Validando columnas con valores constantes
unicos=du.data.nunique()
unicos[unicos==1]
Out[21]:
wertyj    1
dtype: int64

La columna wertyj tiene valores constantes, por lo tanto se eliminará.

In [22]:
du.data = du.data.drop(columns=list(unicos[unicos==1].index))
In [23]:
nulos = du.data.isnull().sum()
cant_unicos = du.data.apply(lambda x: len(x.unique()))
porce = nulos/filas
nulos = pd.DataFrame({'nulos':nulos, 'porc':porce, 'cant_unicos': cant_unicos})
# Se contarán las filas que contengan algún dato nulo
al_menos_un_nulo=du.data.isnull().any(axis=1).sum()
nulos.sort_values(by='porc', ascending=False)
Out[23]:
nulos porc cant_unicos
yr_renovated 21069 0.159621 70
sqft_basement 13368 0.101277 306
price 30 0.000227 4027
zipcode 12 0.000091 71
sqft_above 12 0.000091 947
sqft_living 11 0.000083 1039
lat 9 0.000068 5035
jhygtf 9 0.000068 71
floors 9 0.000068 7
grade 9 0.000068 13
bathrooms 8 0.000061 31
yr_built 7 0.000053 117
waterfront 6 0.000045 3
sqft_lot 6 0.000045 9782
bedrooms 5 0.000038 14
condition 5 0.000038 6
view 4 0.000030 6
date 3 0.000023 373
long 3 0.000023 753
sqft_living15 1 0.000008 778
sqft_lot15 1 0.000008 8690
index 0 0.000000 21999
In [24]:
print(f'De {filas} registros, hay {al_menos_un_nulo} registros con al menos un valor nulo, representando el {al_menos_un_nulo/filas:.2%}')
De 131994 registros, hay 21531 registros con al menos un valor nulo, representando el 16.31%

Se debe tener en cuenta que para el caso de yr_renovated y sqft_basement, un valor nulo no representa necesariamente falta de información, para el caso de yr_renovated, un nulo representa que esa casa nunca se renovó. Y en el caso de sqft_basement quiere decir que la casa no tiene sótano.

In [25]:
# Calculando variables adicionales
du.data = calculo_variables_adicionales(du.data)
In [26]:
# Esta vez no se tendrán en cuenta las columnas yr_renovated y sqft_basement
df = du.data.drop(columns=['yr_renovated', 'sqft_basement'])
nulos = df.isnull().sum()
cant_unicos = df.apply(lambda x: len(x.unique()))
porce = nulos/filas
nulos = pd.DataFrame({'nulos':nulos, 'porc':porce, 'cant_unicos': cant_unicos})
# Se contarán las filas que contengan algún dato nulo
al_menos_un_nulo=df.isnull().any(axis=1).sum()
nulos.sort_values(by='porc', ascending=False)
Out[26]:
nulos porc cant_unicos
price 30 0.000227 4027
sqft_above 12 0.000091 947
zipcode 12 0.000091 71
sqft_living 11 0.000083 1039
antiguedad_venta 10 0.000076 118
floors 9 0.000068 7
grade 9 0.000068 13
jhygtf 9 0.000068 71
lat 9 0.000068 5035
bathrooms 8 0.000061 31
yr_built 7 0.000053 117
waterfront 6 0.000045 3
sqft_lot 6 0.000045 9782
bedrooms 5 0.000038 14
condition 5 0.000038 6
view 4 0.000030 6
date 3 0.000023 373
long 3 0.000023 753
yr_date 3 0.000023 3
sqft_living15 1 0.000008 778
sqft_lot15 1 0.000008 8690
tiene_sotano 0 0.000000 2
fue_renovada 0 0.000000 2
index 0 0.000000 21999
In [27]:
print(f'De {filas} registros, hay {al_menos_un_nulo} registros con al menos un valor nulo, representando el {al_menos_un_nulo/filas:.2%}')
De 131994 registros, hay 109 registros con al menos un valor nulo, representando el 0.08%

Para el entrenamiento del primer modelo se eliminarán los datos nulos debido a su poca cantidad.

In [28]:
columnas_a_eliminar_nulos = du.data.drop(columns=['yr_renovated', 'sqft_basement']).columns
du.data = procesamiento_datos_faltantes(du.data, columnas_a_eliminar_nulos)
In [29]:
print(f'{du.data.shape=}')
du.data.shape=(21890, 26)

Dividir el dataset en Training set y Test set¶

Este paso debe ser al inicio de los proyectos, Se deben realizar todas las transformaciones, preparación de datos y limpieza de los datos, en el train set y en la evaluacion se deben aplicarl al test set y a los datos nuevos que lleguen al sistema. Esta division inicial se hace para evitar data leakage de los datos de test a los datos de train, por ejemplo en las imputaciones.

Por este motivo se realizara en esta parte.

Division de los datos de entrenamiento (Train set) y de Evaluacion (test - set), las divisiones utilizadas son

  • 80% (train) , 20%(test)
  • 70% (train) , 30%(test)

El Train set se hace para seleccion de Modelos y el Test-set solamente para la evaluacion Final

In [30]:
train_test, validation = train_test_split(du.data, test_size=0.2, random_state=1)
du.save_data(train_test, du.raw_train_test_path)
du.save_data(validation, du.raw_validation_path)
print(f'{train_test.shape=}')
print(f'{validation.shape=}')
train_test.shape=(17512, 26)
validation.shape=(4378, 26)

Descripcion y Limpieza de los datos¶

In [31]:
du.data = du.load_data(du.raw_train_test_path)
print('Tipos de variables')
du.data.info()
Tipos de variables
<class 'pandas.core.frame.DataFrame'>
Int64Index: 17512 entries, 19857 to 237
Data columns (total 26 columns):
 #   Column            Non-Null Count  Dtype         
---  ------            --------------  -----         
 0   index             17512 non-null  int64         
 1   zipcode           17512 non-null  float64       
 2   grade             17512 non-null  float64       
 3   sqft_basement     6857 non-null   float64       
 4   view              17512 non-null  float64       
 5   bathrooms         17512 non-null  float64       
 6   bedrooms          17512 non-null  float64       
 7   sqft_above        17512 non-null  float64       
 8   sqft_living15     17512 non-null  float64       
 9   lat               17512 non-null  float64       
 10  waterfront        17512 non-null  float64       
 11  floors            17512 non-null  float64       
 12  date              17512 non-null  datetime64[ns]
 13  yr_renovated      732 non-null    float64       
 14  yr_built          17512 non-null  float64       
 15  long              17512 non-null  float64       
 16  jhygtf            17512 non-null  float64       
 17  sqft_lot          17512 non-null  float64       
 18  price             17512 non-null  float64       
 19  condition         17512 non-null  float64       
 20  sqft_lot15        17512 non-null  float64       
 21  sqft_living       17512 non-null  float64       
 22  tiene_sotano      17512 non-null  int32         
 23  fue_renovada      17512 non-null  int32         
 24  yr_date           17512 non-null  float64       
 25  antiguedad_venta  17512 non-null  float64       
dtypes: datetime64[ns](1), float64(22), int32(2), int64(1)
memory usage: 3.5 MB

Identificacion de Variables¶

  • Variables de entrada y de salida
  • Tipo de Variables (categoricas o Numericas)
  • Tipo de datos (int, float, string, factor, boolean, ...)
In [32]:
# Clasificación de columnas
clasificacion_columnas = {
    'categorica_ordinal': ['zipcode', 'grade', 'view', 'waterfront', 'condition', 'lat', 'long'],
    'fecha': ['date'],
    'id': ['index'],
    'numerica_continua': ['sqft_basement', 'sqft_above', 'sqft_living15', 'sqft_lot', 'price', 'sqft_lot15', 'sqft_living'],
    'numerica_discreta': ['bathrooms', 'bedrooms', 'yr_renovated', 'yr_built', 'jhygtf', 'yr_date', 'antiguedad_venta', 'floors']
}
columna_salida = 'price'
columnas_a_descartar = ['date', 'index']

du.data = du.data.drop(columns=columnas_a_descartar)

columnas_entrada = du.data.drop(columns=columna_salida).columns
print(f'Columna salida: {columna_salida}')
print(f'Columnas de entrada: {columnas_entrada}')
Columna salida: price
Columnas de entrada: Index(['zipcode', 'grade', 'sqft_basement', 'view', 'bathrooms', 'bedrooms',
       'sqft_above', 'sqft_living15', 'lat', 'waterfront', 'floors',
       'yr_renovated', 'yr_built', 'long', 'jhygtf', 'sqft_lot', 'condition',
       'sqft_lot15', 'sqft_living', 'tiene_sotano', 'fue_renovada', 'yr_date',
       'antiguedad_venta'],
      dtype='object')

Analisis General Univariable y Bivariable¶

Analisis de cada una de las variables para lograr calidad de datos en cada columna

  • Correccion del tipo de dato (numericas, categoricas, string) de cada columna (optimizar memoria)
  • Deteccion de numero de datos faltantes
  • Deteccion de duplicados
In [33]:
du.data= clasificar_columnas(du.data, clasificacion_columnas)
In [34]:
du.data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 17512 entries, 19857 to 237
Data columns (total 24 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   zipcode           17512 non-null  int32  
 1   grade             17512 non-null  int32  
 2   sqft_basement     6857 non-null   float64
 3   view              17512 non-null  int32  
 4   bathrooms         17512 non-null  float64
 5   bedrooms          17512 non-null  float64
 6   sqft_above        17512 non-null  float64
 7   sqft_living15     17512 non-null  float64
 8   lat               17512 non-null  int32  
 9   waterfront        17512 non-null  int32  
 10  floors            17512 non-null  float64
 11  yr_renovated      732 non-null    float64
 12  yr_built          17512 non-null  float64
 13  long              17512 non-null  int32  
 14  jhygtf            17512 non-null  float64
 15  sqft_lot          17512 non-null  float64
 16  price             17512 non-null  float64
 17  condition         17512 non-null  int32  
 18  sqft_lot15        17512 non-null  float64
 19  sqft_living       17512 non-null  float64
 20  tiene_sotano      17512 non-null  int32  
 21  fue_renovada      17512 non-null  int32  
 22  yr_date           17512 non-null  float64
 23  antiguedad_venta  17512 non-null  float64
dtypes: float64(15), int32(9)
memory usage: 2.7 MB

Eliminación de outliers¶

Primero se analizará la distribución de cada variable¶

In [35]:
box_plots = [plot.box(du.data, y=columna) for columna in clasificacion_columnas['numerica_continua']]
histograms = [plot.histogram(du.data, x=columna, text_auto=False) for columna in clasificacion_columnas['numerica_continua']]

plot.grid_subplot(*box_plots, cols= 3, title= 'Distribución inicial',titles=clasificacion_columnas['numerica_continua']).show()
plot.grid_subplot(*histograms, cols= 3, title= 'Distribución inicial',titles=clasificacion_columnas['numerica_continua']).show()
In [36]:
# Eliminando registros identificados como outliers según el z_score

du.data = du.data[~pd.Series(z_score_outliers(du.data, 'price')[1], index=du.data.index)]
du.data = du.data[~pd.Series(z_score_outliers(du.data, 'sqft_lot')[1], index=du.data.index)]
du.data = du.data[~pd.Series(z_score_outliers(du.data, 'sqft_lot15')[1], index=du.data.index)]
In [37]:
box_plots = [plot.box(du.data, y=columna) for columna in clasificacion_columnas['numerica_continua']]
histograms = [plot.histogram(du.data, x=columna, text_auto=False) for columna in clasificacion_columnas['numerica_continua']]

plot.grid_subplot(*box_plots, cols= 3, title= 'Distribución inicial',titles=clasificacion_columnas['numerica_continua']).show()
plot.grid_subplot(*histograms, cols= 3, title= 'Distribución inicial',titles=clasificacion_columnas['numerica_continua']).show()

Para las variables numericas_discreta se analizarán sus rangos

In [38]:
pd.DataFrame({
    'min':du.data[clasificacion_columnas['numerica_discreta']].min(),
    'max':du.data[clasificacion_columnas['numerica_discreta']].max(),
    'nulos':du.data[clasificacion_columnas['numerica_discreta']].isna().sum()
})
Out[38]:
min max nulos
bathrooms 0.0 8.0 0
bedrooms 0.0 33.0 0
yr_renovated 1934.0 2015.0 15905
yr_built 1900.0 2015.0 0
jhygtf 0.0 2015.0 0
yr_date 2014.0 2015.0 0
antiguedad_venta -1.0 115.0 0
floors 1.0 3.0 0

Definicion de outliers:

Se considerarán outliers:

  • Apartamentos con bathrooms o bedrooms igual a 0
  • Apartamentos con bedrooms mayor a 5
  • Apartamentos con bathrooms mayor a 4
In [39]:
bathrooms_outliers = (du.data['bathrooms']==0) | (du.data['bathrooms'] > 4)
bedrooms_outliers = (du.data['bedrooms']==0) | (du.data['bedrooms'] > 5)
son_outliers = bathrooms_outliers | bedrooms_outliers
cant_outliers = son_outliers.sum()
print(f'Con estas características hay: {cant_outliers} outliers representando el {cant_outliers/len(son_outliers):.2%}')
Con estas características hay: 324 outliers representando el 1.96%

Imputación de outliers: Se reemplazarán los outliers calculando la mediana recortada la cual se realiza teniendo en cuenta únicamente los inliers

In [40]:
du.data = mediana_recortada_imputacion(du.data, 'bathrooms', bathrooms_outliers)
du.data = mediana_recortada_imputacion(du.data, 'bedrooms', bedrooms_outliers)
In [41]:
# Revisando los datos después de la imputación
plot.grid_subplot(
    *[plot.bar(du.data, x=column, max_bins=10) for column in clasificacion_columnas['numerica_discreta']], 
    cols=3, 
    titles=clasificacion_columnas['numerica_discreta']).show()
pd.DataFrame({
    'min':du.data[clasificacion_columnas['numerica_discreta']].min(),
    'max':du.data[clasificacion_columnas['numerica_discreta']].max(),
    'nulos':du.data[clasificacion_columnas['numerica_discreta']].isna().sum()
})
Out[41]:
min max nulos
bathrooms 1.0 4.0 0
bedrooms 1.0 5.0 0
yr_renovated 1934.0 2015.0 15905
yr_built 1900.0 2015.0 0
jhygtf 0.0 2015.0 0
yr_date 2014.0 2015.0 0
antiguedad_venta -1.0 115.0 0
floors 1.0 3.0 0

Para las columnas categóricas se revisarán sus valores únicos.

In [42]:
pd.DataFrame({
    'distinct_count':du.data[clasificacion_columnas['categorica_ordinal']].nunique(),
    'nulos':du.data[clasificacion_columnas['categorica_ordinal']].isna().sum()
})
Out[42]:
distinct_count nulos
zipcode 70 0
grade 12 0
view 5 0
waterfront 2 0
condition 5 0
lat 429 0
long 608 0

No se evidencian datos atípicos. lat, long y zipcode tienen demasiada cardinalidad pero es normal por ser datos de ubicación.

Eliminar columnas de datos Innecesarios¶

Se realizará un perfilado de los datos para identificar problemas de calidad no identificados anteriormente.

In [43]:
profiler = ProfileReport(du.data, explorative=True)

profiler_to_file(profiler, '2_0_0 Perfilado inicial.html')
Out[43]:
True
In [44]:
# Se utilizará el índice de correlación phik pues este permite calcular la correlación entre variables numéricas y categóricas al tiempo.
# Este indice funciona mejor cuando no hay valores nulos, por lo tanto se reemplazarán los valores nulos por 0
cor_mat = du.data.fillna(0).phik_matrix()
interval columns not set, guessing: ['zipcode', 'grade', 'sqft_basement', 'view', 'bathrooms', 'bedrooms', 'sqft_above', 'sqft_living15', 'lat', 'waterfront', 'floors', 'yr_renovated', 'yr_built', 'long', 'jhygtf', 'sqft_lot', 'price', 'condition', 'sqft_lot15', 'sqft_living', 'tiene_sotano', 'fue_renovada', 'yr_date', 'antiguedad_venta']
In [45]:
# Primero se analizará la correlación entre las variables de entrada y se descartarán aquellas con una correlación superior a 0.9

(cor_mat.loc[columnas_entrada, columnas_entrada] > 0.9).sum().sort_values(ascending=False)
Out[45]:
yr_renovated        3
fue_renovada        3
jhygtf              3
yr_built            2
tiene_sotano        2
sqft_living         2
antiguedad_venta    2
sqft_above          2
sqft_basement       2
sqft_living15       1
lat                 1
waterfront          1
floors              1
grade               1
long                1
bedrooms            1
sqft_lot            1
condition           1
sqft_lot15          1
bathrooms           1
view                1
yr_date             1
zipcode             1
dtype: int64
In [46]:
# Se analizarán con mas detalle aquellas que tienen una alta correlación con más de 1 columna (Consigo misma)
columnas_alta_correlacion = ['yr_renovated', 'fue_renovada', 'jhygtf', 'yr_built', 'tiene_sotano', 'sqft_living', 'antiguedad_venta', 'sqft_above', 'sqft_basement']
px.imshow(
    cor_mat.loc[columnas_alta_correlacion, columnas_alta_correlacion].round(2), 
    color_continuous_scale= 'blues', 
    text_auto=True).show()

La primera columna a eliminar es jhygtf se observa que es la misma variable que yr_renovated. Aunque la columna fue_renovada está calculada con base en yr_renovated se conservarán ambas para posteriormente elegir con cual de las dos se puede obtener un mejor modelo.

Quedan altas correlaciones entre las siguientes columnas:

  • yr_build y antiguedad_venta
  • tiene_sotano y sqft_basement
  • sqft_living y sqft_above
  • sqft_living y sqft_basement

Para las restantes, se conservarán todas para después mediante el cálculo de la importancia de variables escoger cual tiene un mejor poder predictivo.

Entonces solo se eliminará la columna jhygtf

In [47]:
du.data = du.data.drop(columns=['jhygtf'])
In [48]:
# Correlación de todas las variables de entrada con respecto a la salida
cor_mat.loc['price', du.data.columns].sort_values(ascending=False)
Out[48]:
price               1.000000
sqft_living         0.849294
sqft_above          0.705247
sqft_basement       0.700367
grade               0.520938
sqft_living15       0.412334
bathrooms           0.397703
waterfront          0.335567
view                0.324725
floors              0.269777
sqft_lot15          0.200437
bedrooms            0.191491
sqft_lot            0.144752
zipcode             0.130665
fue_renovada        0.114474
yr_renovated        0.114474
tiene_sotano        0.107443
antiguedad_venta    0.070706
yr_built            0.069420
condition           0.042359
long                0.006108
lat                 0.000000
yr_date                  NaN
Name: price, dtype: float64

Selección de variables¶

Se calculará la importancia de las variables de entrada con respecto al precio adicionando una columna dummy que contrendrá valores aleatorios y se eliminarán aquellas variables cuya importancia sea inferior a la columna aleatoria (Al ser esta aleatoria, sabemos desde el principio que esta no puede ser una buena predictora).

Para realizar este calculo se utilizará un regresor lineal, se debe escalar primero las variables de entrada para poder comparar sus coeficientes.

In [49]:
pipeline_features = make_pipeline(StandardScaler(), LinearRegression())
X = du.data.fillna(0).drop(columns='price')
X['random'] = np.random.normal(size=(X.shape[0], 1))
y = du.data['price']

cv_model = cross_validate(
   pipeline_features, X, y, cv=RepeatedKFold(n_splits=5, n_repeats=5),
   return_estimator=True, n_jobs=2
)

coefs = pd.DataFrame(
   [model[1].coef_ for model in cv_model['estimator']],
   columns=X.columns
)
print()
print(f'mean_test_score: {np.mean(cv_model["test_score"])}')
print(f'std_test_score : {np.std(cv_model["test_score"])}')
mean_test_score: 0.6224194240670496
std_test_score : 0.01638230675298051
In [50]:
coefs
Out[50]:
zipcode grade sqft_basement view bathrooms bedrooms sqft_above sqft_living15 lat waterfront ... long sqft_lot condition sqft_lot15 sqft_living tiene_sotano fue_renovada yr_date antiguedad_venta random
0 -1322.164921 127238.143645 3.152258e+04 28179.302625 11672.913208 -32554.317087 5.788860e+04 20704.660430 -962.693602 47519.509723 ... 1348.178151 -714.262198 17215.973777 -17371.315870 6.827841e+04 6116.012698 -1.014107e+06 8.554486e+03 4.544744e+04 1053.149172
1 -2668.630337 123673.583195 -1.452928e+17 22374.300265 20518.140649 -32425.232249 -2.623152e+17 28384.282390 -1745.477963 52034.925903 ... -271.789253 -689.789307 15305.623886 -17716.997341 2.874424e+17 10819.355130 -1.275895e+06 -9.956917e+12 6.251387e+14 1212.888860
2 -1488.468742 124418.937614 7.872841e+17 23263.492497 17839.550478 -36782.825001 1.421284e+18 25797.469375 -1159.311863 41353.041414 ... -575.781618 882.633784 16502.581728 -19747.913127 -1.564712e+18 6334.818631 -9.261017e+05 1.476191e+15 -9.256125e+16 1448.166299
3 -372.698628 126942.122992 3.159567e+04 24128.708605 14428.005933 -36583.701314 6.432433e+04 21436.140382 -1280.810469 55427.993513 ... 382.976148 1202.401693 17847.671605 -21256.026944 7.428672e+04 7395.757792 -1.033752e+06 9.643610e+03 4.694418e+04 1901.779993
4 -1210.668691 129515.092879 -9.576618e+16 21700.011651 13650.645360 -36528.264565 -1.722925e+17 24664.017070 -156.233605 54926.504929 ... 2105.461344 3563.550678 16692.006171 -22977.532014 1.907435e+17 2904.079844 -1.015748e+06 1.007629e+14 -6.333950e+15 1358.896822
5 -2636.078997 125140.689391 7.028160e+17 25736.474980 14097.500552 -36552.567028 1.265288e+18 22695.523443 -860.403193 52205.096338 ... 1262.264840 305.000467 16409.166294 -19459.218465 -1.399865e+18 4594.874140 -1.124785e+06 -1.246316e+16 7.846655e+17 1286.392398
6 -1913.962709 126441.700328 3.406096e+17 24351.491116 16087.322232 -32811.192962 6.122061e+17 29947.979733 -3102.976242 53400.982923 ... -56.758850 2599.587700 17587.924890 -20657.970720 -6.718651e+17 10791.023628 -1.108786e+06 -4.693272e+14 2.941954e+16 2857.684886
7 -2320.997424 124151.251689 -4.513797e+17 21960.578266 18131.083039 -34966.535542 -8.177648e+17 23809.870738 -1149.163336 48503.812649 ... 353.463260 -890.703570 15656.421599 -18382.473445 9.002656e+17 7957.978424 -9.214478e+05 1.070441e+15 -6.733255e+16 824.602711
8 -2051.917374 127569.071581 -1.917346e+17 24305.868696 15836.593759 -33744.506644 -3.445586e+17 20900.189948 759.918291 52368.762600 ... 982.607905 953.785288 15071.737152 -19831.841802 3.804573e+17 5553.456123 -9.505082e+05 -3.729269e+16 2.345466e+18 2654.918219
9 422.007598 128440.942145 2.043919e+17 24044.519267 14156.581067 -36683.548369 3.715047e+17 24842.474363 -1319.729803 44596.644062 ... 353.318409 922.171142 19039.677092 -20193.264534 -4.084792e+17 6064.098900 -1.134510e+06 6.127499e+14 -3.837956e+16 -24.221765
10 -536.554960 130473.324491 3.143595e+04 26832.965876 13778.729948 -35272.079700 5.723329e+04 25185.230660 -1371.203811 49684.145790 ... 893.484563 868.353476 17181.583563 -18302.664839 6.775879e+04 7345.194339 -7.015375e+05 1.060770e+04 4.617860e+04 1023.021633
11 399.019288 121396.493796 3.653492e+04 20861.211259 14033.493260 -36618.497457 6.094781e+04 22395.170840 -368.593046 54004.238512 ... 380.783846 1919.931365 17308.683480 -22193.007402 7.368077e+04 3350.186736 -1.228554e+06 9.836328e+03 4.601471e+04 314.347587
12 -510.881614 127776.202382 7.245604e+17 24693.218495 16843.366771 -33386.025088 1.310060e+18 24077.286937 -576.815912 51645.475071 ... -205.741037 2504.523808 17429.253970 -21828.220579 -1.444850e+18 7940.909501 -1.166525e+06 -1.245585e+16 7.822121e+17 1894.067930
13 -2731.798097 126263.158979 1.722558e+17 23463.610652 15763.549590 -37963.511541 3.124724e+17 21438.190738 -961.038809 47079.345654 ... 1267.204327 -1322.265279 15640.846522 -17969.105272 -3.430161e+17 7354.037639 -9.927979e+05 -2.064255e+14 1.296377e+16 1900.554324
14 -2885.603341 126374.771241 2.805051e+04 24407.533424 16850.739235 -32090.389614 5.629477e+04 26712.162616 -1819.525637 48702.079091 ... 447.134329 318.320557 16112.111971 -18492.955961 6.516477e+04 7689.109775 -1.158750e+06 8.311401e+03 4.750386e+04 2027.034919
15 -32.562158 127213.825855 3.398261e+04 24393.669528 13441.525749 -36108.081718 6.336721e+04 20590.645933 -1674.104139 51135.757840 ... 816.113544 2672.328304 15705.004990 -19701.247798 7.459472e+04 5067.326285 -9.988954e+05 9.472844e+03 4.680580e+04 2399.805588
16 -2368.465037 124132.022587 2.370425e+16 22713.629475 14586.918489 -35615.173706 4.266847e+16 23646.625499 -465.150353 55313.902844 ... -529.796079 1644.676435 17755.556924 -20867.342555 -4.707114e+16 8210.442969 -9.284336e+05 -2.004684e+16 1.266002e+18 1341.975881
17 -2563.608121 129140.938121 1.129663e+17 20979.686984 16306.685515 -32973.931180 2.064256e+17 27036.644646 -628.185954 52898.757011 ... 1426.321810 -1124.074298 16931.553522 -17875.863155 -2.269175e+17 9954.384085 -1.223379e+06 -6.011303e+16 3.765282e+18 460.284655
18 -1674.418093 123869.139771 6.058282e+17 23889.896738 15511.890216 -37249.940317 1.080535e+18 23134.303456 -900.553550 43917.883529 ... 961.261192 1889.582969 16377.327738 -21503.739269 -1.190783e+18 4543.165091 -9.661453e+05 -1.755710e+14 1.103848e+16 2193.122737
19 -2818.431376 128513.361133 1.520944e+18 27987.155720 18654.142092 -31850.668503 2.712892e+18 26568.001302 -1917.270608 47011.308254 ... -551.193472 695.398397 17190.064150 -20281.755364 -2.999659e+18 7795.220754 -1.142911e+06 2.055931e+15 -1.287650e+17 199.074792
20 -852.751317 129006.118652 3.093680e+04 27873.874646 15656.579421 -35849.574192 5.921205e+04 24937.480678 -2670.414000 47812.705147 ... 821.367859 3399.300022 17127.026595 -21638.372930 6.927857e+04 6811.797861 -9.320988e+05 1.005204e+04 4.717715e+04 2124.751166
21 -2716.051510 129145.955969 5.596288e+17 24543.965601 13795.052581 -38213.051244 1.010483e+18 18642.645450 -796.673897 44854.672253 ... -377.179245 -2855.297235 15450.754095 -16975.504516 -1.110566e+18 4136.864447 -1.235302e+06 -1.410847e+15 8.828146e+16 1588.621725
22 -1552.580453 126600.352530 2.307603e+04 23221.298794 19274.809887 -32938.512629 5.696524e+04 25870.409728 -693.682641 53114.893874 ... 172.482794 1200.489037 17010.423046 -18344.411782 6.333709e+04 11882.871706 -1.142261e+06 8.249172e+03 4.622982e+04 1076.473868
23 -1872.635861 125558.903649 3.740584e+04 22275.320338 12473.799620 -34325.247874 5.677262e+04 24609.599036 -393.473343 53112.530779 ... 1247.459751 670.847401 16564.075389 -20505.469045 7.033532e+04 4006.353410 -8.095379e+05 9.366401e+03 4.679957e+04 1849.298281
24 -159.803380 121832.758839 -4.487960e+17 22449.873741 16026.427248 -34268.115217 -8.142828e+17 26238.518002 -282.690776 52300.771617 ... 1038.328708 1416.754900 17790.296828 -21124.647818 8.982402e+17 7069.621852 -1.195453e+06 4.327391e+15 -2.721971e+17 865.825089

25 rows × 23 columns

In [51]:
px.box(coefs, orientation='h', title='Importancia de los coeficientes y sus variaciones')
In [52]:
# Debido a que no se normalizó el precio, se dividirán los coeficientes por la media del precio para tenerlos en una escala mas
# manejable

coefs_resumen = pd.DataFrame({
    'variacion': (coefs.std()/du.data['price'].mean()),
    'media': coefs.mean()/du.data['price'].mean(),
    'media_absoluta': coefs.abs().mean()/du.data['price'].mean()
})
coef_variacion = coefs_resumen['variacion'].sort_values(ascending=False)
coef_variacion
Out[52]:
antiguedad_venta    1.776458e+12
yr_built            1.776376e+12
sqft_living         1.684384e+12
sqft_above          1.526016e+12
sqft_basement       8.502111e+11
yr_date             2.831498e+10
fue_renovada        2.804958e-01
yr_renovated        2.797004e-01
waterfront          7.586745e-03
sqft_living15       5.238569e-03
grade               4.694152e-03
tiene_sotano        4.608884e-03
bathrooms           4.230300e-03
view                4.003223e-03
bedrooms            3.829334e-03
sqft_lot15          3.255856e-03
floors              3.073753e-03
sqft_lot            3.024239e-03
zipcode             2.094296e-03
condition           1.863360e-03
lat                 1.592778e-03
random              1.483685e-03
long                1.418870e-03
Name: variacion, dtype: float64

Para aquellas variables con demasiada variación no es confiable tomar el promedio como su importancia, por lo tanto no serán eliminadas.

Se analizarán aquellas con una variación inferior a 0.7

In [53]:
coefs_confiables = coef_variacion[coef_variacion <= 0.7]
coefs_confiables
Out[53]:
fue_renovada     0.280496
yr_renovated     0.279700
waterfront       0.007587
sqft_living15    0.005239
grade            0.004694
tiene_sotano     0.004609
bathrooms        0.004230
view             0.004003
bedrooms         0.003829
sqft_lot15       0.003256
floors           0.003074
sqft_lot         0.003024
zipcode          0.002094
condition        0.001863
lat              0.001593
random           0.001484
long             0.001419
Name: variacion, dtype: float64
In [54]:
px.box(coefs[coefs_confiables.index], orientation='h', title='Importancia de los coeficientes y sus variaciones').show()
px.box(coefs.abs()[coefs_confiables.index], orientation='h', title='Importancia absoluta de los coeficientes y sus variaciones').show()

De esta forma se pueden eliminar aquellas que sean consistentemente peor que la columna random.

Al analizar el gráfico, no se pueden identificar variables con un peor desempeño que random.

Se usará f_regression de scikit learn para calcular la importancia de las variables.

In [55]:
from sklearn.feature_selection import f_regression

f_statistic, p_values =  f_regression(X, y)
pd.DataFrame({'f_statistic': f_statistic, 'p_value': p_values.round(3)}, index=X.columns).sort_values(by='f_statistic', ascending=False)
Out[55]:
f_statistic p_value
sqft_living 13633.682142 0.000
grade 11295.852987 0.000
sqft_above 7942.786595 0.000
sqft_living15 7622.345514 0.000
bathrooms 3727.835576 0.000
view 2569.742164 0.000
sqft_basement 1741.583489 0.000
bedrooms 1368.668278 0.000
waterfront 1276.563745 0.000
floors 842.686506 0.000
tiene_sotano 509.942279 0.000
sqft_lot15 363.784440 0.000
sqft_lot 331.488959 0.000
yr_renovated 224.215157 0.000
fue_renovada 223.038145 0.000
zipcode 36.987037 0.000
condition 35.840032 0.000
antiguedad_venta 34.136911 0.000
yr_built 34.030063 0.000
lat 0.912105 0.340
long 0.479515 0.489
random 0.426350 0.514
yr_date 0.350826 0.554

Con esta técnica, se puede determinar el umbral desde el que se van a descartar las variables, se observa que ninguna variable tiene un poder predictivo peor que la variable random.

Pero se puede ver que la importancia de las variables lat, long y yr_date no son confiables al tener un p-value mayor a 0.05

Adicionalmente, podemos determinar que de las variables de entrada altamente correlacionadas, se pueden eliminar las siguientes

  • antiguedad_venta > yr_built
  • sqft_basement > tiene_sotano
  • sqft_living > sqft_above
  • sqft_living > sqft_basement

Para yr_renovated y fue_renovada la diferencia es muy poca, por lo tanto se escogerá fue_renovada por la alta cantidad de nulos de yr_renovated

Por lo tanto se descartarán las siguientes columnas:

lat, long, yr_date, yr_built, tiene_sotano, sqft_above, sqft_basement, yr_built y yr_renovated

In [56]:
du.data = du.data.drop(columns=['lat', 'long', 'yr_date', 'yr_built', 'tiene_sotano', 'sqft_above', 'sqft_basement', 'yr_built', 'yr_renovated'])

Procesamiento de Datos Faltantes¶

Ya que se eliminaron las columnas yr_renovated y sqft_basement, se puede omitir este paso pues ya no quedan mas nulos.

In [57]:
du.data.isnull().sum().sort_values(ascending=False)
Out[57]:
zipcode             0
grade               0
view                0
bathrooms           0
bedrooms            0
sqft_living15       0
waterfront          0
floors              0
sqft_lot            0
price               0
condition           0
sqft_lot15          0
sqft_living         0
fue_renovada        0
antiguedad_venta    0
dtype: int64

Analisis Univariable¶

Estadistico Descriptico y Analisis

Variables Numericas¶

| Tendencia Central | Medida de Dispersión | Visualizacion | |:-----------------:|:-------------------------:|:-------------:| | Media | Rango | Histogramas | | Mediana | Cuartiles | Boxplots | | Moda | Rango inter cuartil (IQR) | | | Minimo | Varianza | | | Maximo | Desviacion Estandard | | | . | Skewness | | | . | Kurtosis | |

In [58]:
calcular_descriptivas(du.data)
Out[58]:
zipcode grade view bathrooms bedrooms sqft_living15 waterfront floors sqft_lot price condition sqft_lot15 sqft_living fue_renovada antiguedad_venta
count 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000 1.655400e+04 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000
mean 98079.010269 7.583968 0.200254 1.713121 3.306572 1944.150840 0.005739 1.436873 9934.879667 5.106034e+05 3.408058 8996.247856 2016.595143 0.039205 43.645524
std 53.580311 1.103965 0.704138 0.674940 0.821211 649.237427 0.075539 0.552370 10957.363161 3.238058e+05 0.648368 7636.814694 848.705398 0.194088 29.345804
min 98001.000000 1.000000 0.000000 1.000000 1.000000 460.000000 0.000000 1.000000 520.000000 7.500000e+04 1.000000 659.000000 290.000000 0.000000 -1.000000
25% 98033.000000 7.000000 0.000000 1.000000 3.000000 1470.000000 0.000000 1.000000 5000.000000 3.160000e+05 3.000000 5011.250000 1400.000000 0.000000 18.000000
50% 98070.000000 7.000000 0.000000 2.000000 3.000000 1810.000000 0.000000 1.000000 7480.000000 4.400000e+05 3.000000 7500.000000 1880.000000 0.000000 40.000000
75% 98118.000000 8.000000 0.000000 2.000000 4.000000 2300.000000 0.000000 2.000000 10140.000000 6.200000e+05 4.000000 9750.000000 2478.750000 0.000000 63.000000
max 98199.000000 13.000000 4.000000 4.000000 5.000000 5790.000000 1.000000 3.000000 137214.000000 7.700000e+06 5.000000 57140.000000 12050.000000 1.000000 115.000000
rango 198.000000 12.000000 4.000000 3.000000 4.000000 5330.000000 1.000000 2.000000 136694.000000 7.625000e+06 4.000000 56481.000000 11760.000000 1.000000 116.000000
IQR 85.000000 1.000000 0.000000 1.000000 1.000000 830.000000 0.000000 1.000000 5140.000000 3.040000e+05 1.000000 4738.750000 1078.750000 0.000000 45.000000
coef de var 0.000546 0.145566 3.516232 0.393982 0.248357 0.333944 13.162944 0.384425 1.102919 6.341630e-01 0.190246 0.848889 0.420861 4.950597 0.672367
skewness 0.372547 0.716523 3.707596 0.644071 0.045938 1.059620 13.087759 0.776642 4.698177 4.658301e+00 1.038248 3.168057 1.323768 4.748876 0.448007
kurtosis -0.884930 1.371933 13.379400 0.237942 -0.103048 1.455089 169.309900 -0.465117 31.240935 5.059510e+01 0.540616 11.847458 4.451355 20.554309 -0.669613
In [59]:
# Actualizando la clasificación de columnas para dejar solo las que están en el dataframe
clasificacion_columnas['numerica_continua'] = list(set(clasificacion_columnas['numerica_continua']).intersection(du.data.columns))
clasificacion_columnas['categorica_ordinal'] = list(set(clasificacion_columnas['categorica_ordinal']).intersection(du.data.columns))
clasificacion_columnas['numerica_discreta'] = list(set(clasificacion_columnas['numerica_discreta']).intersection(du.data.columns))


columnas_a_graficar = clasificacion_columnas['numerica_continua']
In [60]:
plots1 = [plot.box(du.data, y=variable_numerica) for variable_numerica in columnas_a_graficar] 
plot.grid_subplot(*plots1, cols=3, title='Diagramas de cajas y bigotes', titles=columnas_a_graficar).show()

plots2 = [plot.histogram(du.data, x=variable_numerica) for variable_numerica in columnas_a_graficar]
plot.grid_subplot(*plots2, cols=3, title='Histogramas', titles=columnas_a_graficar).show()

Se observan datos asimétricos para todas las columnas numéricas.

Variables Categoricas¶

  • Numero de elementos por categoria
  • Porcentaje de elementos por categoria
  • Graficos de barras
In [61]:
resumen = []

for variable_categorica in clasificacion_columnas['categorica_ordinal']:
    col = du.data[variable_categorica]
    elms_cat = col.groupby(by=col).agg('count')
    total = elms_cat.sum()
    porc = elms_cat / total
    porc.name = 'porc'
    df = pd.DataFrame([elms_cat, porc]).transpose()
    resumen.append(df)

for tabla in resumen:
    display(HTML(tabla.to_html()))
    variable = tabla.columns[0]
    fig = px.bar(tabla[variable], orientation='h', title=str(variable))
    fig.show()
zipcode porc
zipcode
98001 291.0 0.017579
98002 160.0 0.009665
98003 213.0 0.012867
98004 205.0 0.012384
98005 132.0 0.007974
98006 377.0 0.022774
98007 113.0 0.006826
98008 226.0 0.013652
98010 69.0 0.004168
98011 150.0 0.009061
98014 65.0 0.003927
98019 131.0 0.007913
98022 140.0 0.008457
98023 399.0 0.024103
98024 31.0 0.001873
98027 265.0 0.016008
98028 220.0 0.013290
98029 268.0 0.016189
98030 198.0 0.011961
98031 219.0 0.013229
98032 106.0 0.006403
98033 307.0 0.018545
98034 446.0 0.026942
98038 431.0 0.026036
98039 29.0 0.001752
98040 187.0 0.011296
98042 431.0 0.026036
98045 150.0 0.009061
98052 467.0 0.028211
98053 287.0 0.017337
98055 221.0 0.013350
98056 322.0 0.019451
98058 361.0 0.021807
98059 381.0 0.023016
98065 238.0 0.014377
98070 54.0 0.003262
98072 216.0 0.013048
98074 336.0 0.020297
98075 274.0 0.016552
98077 131.0 0.007913
98092 250.0 0.015102
98102 88.0 0.005316
98103 477.0 0.028815
98105 166.0 0.010028
98106 275.0 0.016612
98107 211.0 0.012746
98108 153.0 0.009242
98109 85.0 0.005135
98112 166.0 0.010028
98115 476.0 0.028754
98116 259.0 0.015646
98117 442.0 0.026700
98118 406.0 0.024526
98119 141.0 0.008518
98122 209.0 0.012625
98125 331.0 0.019995
98126 273.0 0.016491
98133 402.0 0.024284
98136 209.0 0.012625
98144 254.0 0.015344
98146 228.0 0.013773
98148 51.0 0.003081
98155 372.0 0.022472
98166 200.0 0.012082
98168 218.0 0.013169
98177 192.0 0.011598
98178 211.0 0.012746
98188 108.0 0.006524
98198 220.0 0.013290
98199 234.0 0.014136
grade porc
grade
1 1.0 0.000060
3 3.0 0.000181
4 23.0 0.001389
5 183.0 0.011055
6 1592.0 0.096170
7 7145.0 0.431618
8 4770.0 0.288148
9 1881.0 0.113628
10 698.0 0.042165
11 211.0 0.012746
12 40.0 0.002416
13 7.0 0.000423
waterfront porc
waterfront
0 16459.0 0.994261
1 95.0 0.005739
condition porc
condition
1 23.0 0.001389
2 126.0 0.007611
3 10763.0 0.650175
4 4357.0 0.263199
5 1285.0 0.077625
view porc
view
0 15123.0 0.913556
1 251.0 0.015162
2 666.0 0.040232
3 324.0 0.019572
4 190.0 0.011478

Analisis Bivariable¶

Estadistico Descriptico y Analisis

Numericas vs Numericas¶

  • Scatter Plot
  • Heatmap
  • Correlacion
In [62]:
columnas_a_graficar = list(filter(lambda x: x!='price', clasificacion_columnas['numerica_continua']))

plots = [plot.scatter(du.data, x=columna, y='price') for columna in columnas_a_graficar]

plot.grid_subplot(*plots, cols = 2, titles=columnas_a_graficar).show()
In [63]:
clasificacion_columnas
Out[63]:
{'categorica_ordinal': ['zipcode', 'grade', 'waterfront', 'condition', 'view'],
 'fecha': ['date'],
 'id': ['index'],
 'numerica_continua': ['sqft_living',
  'sqft_living15',
  'sqft_lot15',
  'sqft_lot',
  'price'],
 'numerica_discreta': ['antiguedad_venta', 'bathrooms', 'floors', 'bedrooms']}
In [64]:
plots = [
    plot.box(du.data, x=column, y='price', nbins=0)
    for column in clasificacion_columnas['categorica_ordinal'] + clasificacion_columnas['numerica_discreta']
]
plot.grid_subplot(*plots, 
                  cols=2, 
                  titles=clasificacion_columnas['categorica_ordinal'] + clasificacion_columnas['numerica_discreta'], 
                  title='Box plot entre entradas categóricas y precio',
                  height=1500
                 ).show()

De las variables categóricas, las que parecen tener un mayor impacto en el precio son grade, view y y waterfront. La variable condicion parece tener un aumento en el precio cuando es mayor a 3.

In [65]:
from pandas import DataFrame

corr_matrix: DataFrame = du.data.phik_matrix()
interval columns not set, guessing: ['zipcode', 'grade', 'view', 'bathrooms', 'bedrooms', 'sqft_living15', 'waterfront', 'floors', 'sqft_lot', 'price', 'condition', 'sqft_lot15', 'sqft_living', 'fue_renovada', 'antiguedad_venta']
In [66]:
corr_matrix['price'].sort_values(ascending=False)
Out[66]:
price               1.000000
sqft_living         0.849294
grade               0.520938
sqft_living15       0.412334
bathrooms           0.397703
waterfront          0.335567
view                0.324725
floors              0.269777
sqft_lot15          0.200437
bedrooms            0.191491
sqft_lot            0.144752
zipcode             0.130665
fue_renovada        0.114474
antiguedad_venta    0.070706
condition           0.042359
Name: price, dtype: float64

Las variables relacionadas con el tamaño del apartamento y la calificación tienen mayor correlación con el precio del apartamento.

In [67]:
# Ordenando matriz de correlación con respecto a precio
corr_matrix = corr_matrix.sort_values(by='price', ascending=False)
corr_matrix = corr_matrix.reindex(columns=corr_matrix.index)
sns.heatmap(corr_matrix, cmap='PuOr')
Out[67]:
<AxesSubplot:>
In [68]:
columnas = ['sqft_living', 'sqft_above', 'sqft_basement', 'antiguedad_venta', 'yr_built', 'yr_date', 'yr_renovated', 
            'fue_renovada', 'grade', 'sqft_living15', 'sqft_lot', 'tiene_sotano', 'condition', 'floors', 'bathrooms', 
            'view', 'bedrooms', 'waterfront', 'sqft_lot15', 'zipcode']
In [69]:
profiler2 = ProfileReport(du.data, explorative=True)
profiler_to_file(profiler2, '2_0_1 Perfilado de datos.html')
Out[69]:
True
In [70]:
calcular_descriptivas(du.data)
Out[70]:
zipcode grade view bathrooms bedrooms sqft_living15 waterfront floors sqft_lot price condition sqft_lot15 sqft_living fue_renovada antiguedad_venta
count 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000 1.655400e+04 16554.000000 16554.000000 16554.000000 16554.000000 16554.000000
mean 98079.010269 7.583968 0.200254 1.713121 3.306572 1944.150840 0.005739 1.436873 9934.879667 5.106034e+05 3.408058 8996.247856 2016.595143 0.039205 43.645524
std 53.580311 1.103965 0.704138 0.674940 0.821211 649.237427 0.075539 0.552370 10957.363161 3.238058e+05 0.648368 7636.814694 848.705398 0.194088 29.345804
min 98001.000000 1.000000 0.000000 1.000000 1.000000 460.000000 0.000000 1.000000 520.000000 7.500000e+04 1.000000 659.000000 290.000000 0.000000 -1.000000
25% 98033.000000 7.000000 0.000000 1.000000 3.000000 1470.000000 0.000000 1.000000 5000.000000 3.160000e+05 3.000000 5011.250000 1400.000000 0.000000 18.000000
50% 98070.000000 7.000000 0.000000 2.000000 3.000000 1810.000000 0.000000 1.000000 7480.000000 4.400000e+05 3.000000 7500.000000 1880.000000 0.000000 40.000000
75% 98118.000000 8.000000 0.000000 2.000000 4.000000 2300.000000 0.000000 2.000000 10140.000000 6.200000e+05 4.000000 9750.000000 2478.750000 0.000000 63.000000
max 98199.000000 13.000000 4.000000 4.000000 5.000000 5790.000000 1.000000 3.000000 137214.000000 7.700000e+06 5.000000 57140.000000 12050.000000 1.000000 115.000000
rango 198.000000 12.000000 4.000000 3.000000 4.000000 5330.000000 1.000000 2.000000 136694.000000 7.625000e+06 4.000000 56481.000000 11760.000000 1.000000 116.000000
IQR 85.000000 1.000000 0.000000 1.000000 1.000000 830.000000 0.000000 1.000000 5140.000000 3.040000e+05 1.000000 4738.750000 1078.750000 0.000000 45.000000
coef de var 0.000546 0.145566 3.516232 0.393982 0.248357 0.333944 13.162944 0.384425 1.102919 6.341630e-01 0.190246 0.848889 0.420861 4.950597 0.672367
skewness 0.372547 0.716523 3.707596 0.644071 0.045938 1.059620 13.087759 0.776642 4.698177 4.658301e+00 1.038248 3.168057 1.323768 4.748876 0.448007
kurtosis -0.884930 1.371933 13.379400 0.237942 -0.103048 1.455089 169.309900 -0.465117 31.240935 5.059510e+01 0.540616 11.847458 4.451355 20.554309 -0.669613

Analisis Univariable y Bivariable Final¶

In [71]:
profiler3 = ProfileReport(du.data, explorative=True)
In [72]:
profiler_to_file(profiler3, '2_0_2 Transformacion_final.html')
profiler3
Summarize dataset:   0%|          | 0/5 [00:00<?, ?it/s]
Generate report structure:   0%|          | 0/1 [00:00<?, ?it/s]
Render HTML:   0%|          | 0/1 [00:00<?, ?it/s]
Out[72]:

In [73]:
du.data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 16554 entries, 19857 to 237
Data columns (total 15 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   zipcode           16554 non-null  int32  
 1   grade             16554 non-null  int32  
 2   view              16554 non-null  int32  
 3   bathrooms         16554 non-null  float64
 4   bedrooms          16554 non-null  float64
 5   sqft_living15     16554 non-null  float64
 6   waterfront        16554 non-null  int32  
 7   floors            16554 non-null  float64
 8   sqft_lot          16554 non-null  float64
 9   price             16554 non-null  float64
 10  condition         16554 non-null  int32  
 11  sqft_lot15        16554 non-null  float64
 12  sqft_living       16554 non-null  float64
 13  fue_renovada      16554 non-null  int32  
 14  antiguedad_venta  16554 non-null  float64
dtypes: float64(9), int32(6)
memory usage: 1.6 MB

Variables de entrada¶

  • zipcode
  • grade
  • view
  • bathrooms
  • bedrooms
  • sqft_living15
  • waterfront
  • floors
  • sqft_lot
  • condition
  • sqft_lot15
  • sqft_living
  • fue_renovada
  • antiguedad_venta

Variables de salida¶

  • price

MODELAMIENTO DE LOS DATOS (MACHINE LEARNING)¶

In [74]:
variables_entrada = ['zipcode', 'grade', 'view', 'bathrooms', 'bedrooms', 'sqft_living15', 'waterfront', 'floors', 'sqft_lot', 'condition', 'sqft_lot15', 'sqft_living', 'fue_renovada', 'antiguedad_venta']
variable_salida  = 'price'
du.data = du.data[variables_entrada + [variable_salida]]
In [75]:
y_name = 'price'
x_names = [columna for columna in du.data.columns if not columna == 'price']

Validacion y Evaluacion Cruzada (k-fold Cross Validation)¶

Se hace seleccion de los mejores modelos usando el Training Set y k-fold Cross Validation

In [76]:
alpha = 1
l1_ratio=0.5
normalize=False
max_iter=100000
warm_start=True
modelos_a_probar = {
    'Linear_Regression': {'modelo': make_pipeline(StandardScaler(), LinearRegression())},
    'Linear_Regression degree 2': {'modelo': make_pipeline(
        PolynomialFeatures(degree=2),
        LinearRegression()
    )},
    'Linear_Regression degree 3': {'modelo': make_pipeline(
        PolynomialFeatures(degree=3),
        LinearRegression()
    )},
    'Linear_Regression degree 2 with normalization': {'modelo': make_pipeline(
        StandardScaler(),
        PolynomialFeatures(degree=2),
        LinearRegression()
    )},
    'Lasso': {'modelo': Lasso()},
    'Ridge': {'modelo': make_pipeline(
        StandardScaler(),
        Ridge()
    )},
    'Ridge degree 2': {'modelo': make_pipeline(
        StandardScaler(),
        PolynomialFeatures(degree=2),
        Ridge()
    )},
    'ElasticNet': {'modelo': make_pipeline(
        StandardScaler(),
        ElasticNet(alpha=alpha, l1_ratio=l1_ratio, max_iter=max_iter, warm_start=warm_start)
    )},
    'SGD': {'modelo': make_pipeline(StandardScaler(), SGDRegressor())},
    'SVR': {'modelo': make_pipeline(StandardScaler(), SVR())}
}
In [77]:
for nombre_modelo, dic_modelo in tqdm(modelos_a_probar.items(), desc='Realizando cross validation...'):
    inicial = datetime.now()
    modelo = dic_modelo['modelo']
    dic_modelo['scores'] = cross_val_score(modelo, du.data[x_names], du.data[y_name], cv=5, scoring='r2')
    tiempo_entrenamiento = (datetime.now() - inicial).total_seconds()
    dic_modelo['tiempo_entrenamiento'] = tiempo_entrenamiento
    dic_modelo['media'] = np.mean(dic_modelo['scores'])
    dic_modelo['std'] = np.std(dic_modelo['scores'])
Realizando cross validation...: 100%|██████████| 10/10 [01:23<00:00,  8.35s/it]
In [78]:
tabla_comparativa = pd.DataFrame(modelos_a_probar).transpose()
In [79]:
print('Comparativa R2')
tabla_comparativa.drop(columns=['scores','modelo']).sort_values(by='media', ascending=False)
Comparativa R2
Out[79]:
tiempo_entrenamiento media std
Linear_Regression degree 2 with normalization 1.05808 0.721899 0.017641
Ridge degree 2 0.333026 0.721759 0.017615
Linear_Regression degree 2 1.135979 0.721753 0.017614
Ridge 0.100006 0.621266 0.015699
Linear_Regression 0.140009 0.621266 0.015701
Lasso 0.778806 0.621265 0.015702
SGD 0.211015 0.618611 0.014291
ElasticNet 0.300773 0.575868 0.01368
Linear_Regression degree 3 7.955189 0.240802 0.311652
SVR 71.451784 -0.045957 0.006827

Se escogerá el Ridge degree 2 pues aunque el regresor lineal de grado 2 con normalización obtuvo una media ligeramente mayor, el modelo Ridge de grado 2 lo superó bastante en tiempo de entrenamiento. Como segundo modelo para la optimización de hiper parámetros se utilizará Linear_Regression degree 2 with normalization

In [80]:
mejor_modelo1 = modelos_a_probar['Ridge degree 2']['modelo']
mejor_modelo2 = modelos_a_probar['Linear_Regression degree 2 with normalization']['modelo']
mejor_modelo1.fit(du.data[x_names], du.data[y_name])
Out[80]:
Pipeline(steps=[('standardscaler', StandardScaler()),
                ('polynomialfeatures', PolynomialFeatures()),
                ('ridge', Ridge())])
In [81]:
y_predict = mejor_modelo1.predict(du.data[x_names])
In [82]:
y_real = du.data['price']
In [83]:
vector = np.linspace(y_real.min()*0.8, y_real.max()*1.2)
fig1 = plot.scatter(pd.DataFrame({'y_real': y_real, 'y_pred': y_predict}), x='y_real', y='y_pred')
fig2 = px.line(pd.DataFrame({'y_real': vector, 'y_pred': vector}), x='y_real', y='y_pred')
plot.combine_plots(fig2, fig1).show()

Optimizacion de Hiper parametros (Hyper Parameter optimization)¶

Se seleccionan solo los mejores modelos para realizar el ajuste de hiperparametros, ya que tiene una carga computacional alta.

Al final se obtienen los parametros del mejor modelo

In [84]:
# Obteniendo el nombre de los parámetros del primer modelo
mejor_modelo1.get_params()
Out[84]:
{'memory': None,
 'steps': [('standardscaler', StandardScaler()),
  ('polynomialfeatures', PolynomialFeatures()),
  ('ridge', Ridge())],
 'verbose': False,
 'standardscaler': StandardScaler(),
 'polynomialfeatures': PolynomialFeatures(),
 'ridge': Ridge(),
 'standardscaler__copy': True,
 'standardscaler__with_mean': True,
 'standardscaler__with_std': True,
 'polynomialfeatures__degree': 2,
 'polynomialfeatures__include_bias': True,
 'polynomialfeatures__interaction_only': False,
 'polynomialfeatures__order': 'C',
 'ridge__alpha': 1.0,
 'ridge__copy_X': True,
 'ridge__fit_intercept': True,
 'ridge__max_iter': None,
 'ridge__normalize': 'deprecated',
 'ridge__positive': False,
 'ridge__random_state': None,
 'ridge__solver': 'auto',
 'ridge__tol': 0.001}
In [85]:
# Obteniendo el nombre de los parámetros del segundo modelo
mejor_modelo2.get_params()
Out[85]:
{'memory': None,
 'steps': [('standardscaler', StandardScaler()),
  ('polynomialfeatures', PolynomialFeatures()),
  ('linearregression', LinearRegression())],
 'verbose': False,
 'standardscaler': StandardScaler(),
 'polynomialfeatures': PolynomialFeatures(),
 'linearregression': LinearRegression(),
 'standardscaler__copy': True,
 'standardscaler__with_mean': True,
 'standardscaler__with_std': True,
 'polynomialfeatures__degree': 2,
 'polynomialfeatures__include_bias': True,
 'polynomialfeatures__interaction_only': False,
 'polynomialfeatures__order': 'C',
 'linearregression__copy_X': True,
 'linearregression__fit_intercept': True,
 'linearregression__n_jobs': None,
 'linearregression__normalize': 'deprecated',
 'linearregression__positive': False}
In [86]:
model1_param_grid = [
    {
        'polynomialfeatures__interaction_only': [True, False],
        'ridge__alpha': np.linspace(0.01, 6, 60)
    }
]

model2_param_grid = [
    {
        'polynomialfeatures__interaction_only': [True, False],
        'polynomialfeatures__include_bias': [True, False],
        'linearregression__positive': [True, False]
    }
]
In [87]:
from sklearn.model_selection import GridSearchCV

gs1 = GridSearchCV(mejor_modelo1, model1_param_grid, scoring='r2')
gs2 = GridSearchCV(mejor_modelo2, model2_param_grid, scoring='r2')
In [88]:
# Realizando un grid search para el primer pipeline
print('Grid search primer modelo')
gs1.fit(du.data[x_names], du.data[y_name])

print('Grid search segundo modelo')
# Realizando un grid search para el segundo pipeline
gs2.fit(du.data[x_names], du.data[y_name])
print('Listo')
Grid search primer modelo
Grid search segundo modelo
Listo
In [89]:
print('Los mejores parámetros para el primer pipeline son:')
print(gs1.best_params_)
print()
print('Los mejores parámetros para el segundo pipeline son:')
print(gs2.best_params_)
Los mejores parámetros para el primer pipeline son:
{'polynomialfeatures__interaction_only': False, 'ridge__alpha': 6.0}

Los mejores parámetros para el segundo pipeline son:
{'linearregression__positive': False, 'polynomialfeatures__include_bias': True, 'polynomialfeatures__interaction_only': False}
In [90]:
from sklearn.compose import make_column_transformer

modelo_optimizado = make_pipeline(
        make_column_transformer(
            ('passthrough', [
                'zipcode', 'grade', 'view', 'bathrooms', 'bedrooms', 'sqft_living15', 'waterfront', 'floors',
                'sqft_lot', 'condition', 'sqft_lot15', 'sqft_living', 'fue_renovada', 'antiguedad_venta'
            ])
        ),
        StandardScaler(),
        PolynomialFeatures(degree=2, interaction_only=False),
        Ridge(alpha=6.0)
    )

Evaluacion final del modelo con el Test set¶

Después de haber obtenido el flujo para la transformación de los datos, se empaquetó en un conjunto de pipelines de transformación.

In [91]:
set_validacion = du.load_data(du.raw_validation_path)
set_entrenamiento = du.load_data(du.raw_train_test_path)
In [92]:
from data.procesamiento_datos import Preprocesamiento

pval = Preprocesamiento(['price'], ['price'])
set_validacion_transformado = set_validacion.pipe(preprocessing).pipe(pval.transform).pipe(build_features)[[variable_salida] + variables_entrada]
set_entrenamiento_transformado = set_entrenamiento.pipe(preprocessing).pipe(pval.transform).pipe(build_features)[[variable_salida] + variables_entrada]
In [93]:
modelo_optimizado.fit(set_entrenamiento_transformado[variables_entrada], set_entrenamiento_transformado[variable_salida])

y_real_train, y_predict_train = set_entrenamiento_transformado[variable_salida], modelo_optimizado.predict(set_entrenamiento_transformado[variables_entrada])
y_real_validation, y_predict_validation = set_validacion_transformado[variable_salida], modelo_optimizado.predict(set_validacion_transformado[variables_entrada])
In [94]:
r2_entrenamiento = r2_score(y_real_train, y_predict_train)
r2_validacion = r2_score(y_real_validation, y_predict_validation)

print(f'{r2_entrenamiento=:.2f}')
print(f'{r2_validacion=:.2f}')
r2_entrenamiento=0.73
r2_validacion=0.69
In [95]:
plot.scatter(pd.DataFrame({'y_real_train': y_real_train, 'y_predict_train':y_predict_train}), x='y_real_train', y='y_predict_train').show()

Se observa una disminución en el puntaje R2 de 4 puntos porcentuales, esto muestra que el modelo no tiene overfitting.

Implementacion del Modelo (Deploying)¶

Con el análisis básico y el ajuste hecho, comienza el trabajo real (ingeniería).

El último paso para poner en produccion el modelo de prediccion sera:

  1. Entrenarlo en todo el conjunto de datos nuevamente, para hacer un uso completo de todos los datos disponibles.
  2. Usar los mejores parámetros encontrados mediante la validación cruzada, por supuesto. Esto es muy similar a lo que hicimos al principio, pero esta vez teniendo una idea de su comportamiento y estabilidad. La evaluación se realizó con honestidad, en divisiones distintas de entrenamiento / prueba.

El predictor final se puede serializar y grabar en el disco, de modo que la próxima vez que lo usemos, podemos omitir todo el entrenamiento y usar el modelo capacitado directamente:

In [96]:
from data.procesamiento_datos import Preprocesamiento, LimpiezaCalidad, ProcesamientoDatos
import joblib

#import pickle # Esta es una libreria de serializacion nativa de python, puede tener problemas de seguridad

df_completo = pd.read_csv(du.raw_path.joinpath('kc_house_dataDS.csv'), index_col=0, decimal='.')
# df_transformado = df_completo.pipe(li.fit_transform).pipe(pre_pro.fit_transform).pipe(pval.fit_transform).pipe(build_features)
# df_transformado = df_transformado[[variable_salida] + variables_entrada]
_columnas_numericas = [columna for columna in df_completo.columns if columna != 'date']
pre_pro = Preprocesamiento(['price', 'sqft_lot', 'sqft_lot15'], [])
li = LimpiezaCalidad(_columnas_numericas)
pda = ProcesamientoDatos()

pipeline_tranformacion_prediccion = make_pipeline(
    li, pre_pro, pda
)

pipeline_tranformacion_entrenamiento=make_pipeline(
    li, pre_pro, pval, pda
)

df_transformado = pipeline_tranformacion_entrenamiento.fit_transform(df_completo)
pipeline_tranformacion_prediccion.fit(df_completo)
modelo_optimizado.fit(df_transformado[variables_entrada], df_transformado[variable_salida])

# Guardando
# garbar el modelo en un archivo
joblib.dump(pda, du.model_path.with_stem('pda'))

du.model = modelo_optimizado

Comunicacion de Resultados (Data Story Telling)¶

In [97]:
from sklearn.pipeline import Pipeline
df = pd.read_csv(du.raw_path.joinpath('kc_house_dataDS.csv'), index_col=0, decimal='.')

li = LimpiezaCalidad(_columnas_numericas)
pre_pro = Preprocesamiento(['price', 'sqft_lot', 'sqft_lot15'], [])
pda: ProcesamientoDatos = joblib.load(du.model_path.with_stem('pda'))

pipeline_tranformacion_prediccion = make_pipeline(
    li, pre_pro, pda
)

pipeline_tranformacion_validacion = make_pipeline(
    li, pre_pro, pval, pda
)
model: Pipeline = joblib.load(du.model_path)

df_transformado = pipeline_tranformacion_validacion.transform(df)
y_predict = model.predict(df_transformado)
y_real = df_transformado['price']
In [98]:
is_outlier = y_real.isna()

print(f'{r2_score(y_real[~is_outlier], y_predict[~is_outlier]):.2f}')
0.65
In [99]:
plot.scatter(pd.DataFrame({'y_real': y_real[~is_outlier], 'y_predict': y_predict[~is_outlier]}), x='y_real', y='y_predict').show()

Conclusiones¶

Ayudas Y Referencias¶

  • https://medium.com/@joserzapata/paso-a-paso-en-un-proyecto-machine-learning-bcdd0939d387

  • Proyecto de Principio a Final sobre readmision de pacientes con Diabetes

  • a-complete-machine-learning-walk-through-in-python-part-one

  • a-starter-pack-to-exploratory-data-analysis-with-python-pandas-seaborn-and-scikit-learn

  • a-data-science-for-good-machine-learning-project-walk-through-in-python-part-one

  • Ejemplos de Kaggle

  • END to END ML from data colletion to deployment

Docente: Jose R. Zapata

  • https://joserzapata.github.io
  • https://twitter.com/joserzapata
  • https://www.linkedin.com/in/jose-ricardo-zapata-gonzalez/